#!/usr/bin/perl

# PILU : Configuration Management Software
#        Invoke at first boot and forget
# (c) Nitesh Mor
# Dependency: perl libsvn-perl libalgorithm-diff-perl libarray-diff-perl
#							libfile-copy-recursive-perl

# Typical tree structure
# repo
# |-- global
# |   |-- preseed.cfg
# |   |-- packages.list
#	|		`-- conf_tree
# |       `--etc
# |          `-- pilu
# |              `-- pilu.conf
# |-- local
#	| 	|-- packages.list
#	|		`-- conf_tree
# |       `--etc
# |          `-- pilu
# |              `-- pilu.conf
# |-- desc.local
# `-- desc.global
#

use File::Path;		# For mkpath
use File::Copy::Recursive		qw(dircopy);		# For moving files around
use SVN::Client;	# For svn
use Array::Diff;

# Configuration file: /etc/pilu/pilu.conf
$conf_file = "/etc/pilu/pilu.conf";

# First, assign default values:

# Repository Locations
$CONF{'PROTOCOL'} = "file";
$CONF{'SERVER'} = "";
$CONF{'GLOBAL_CONF_URL'} = "svn/repo/global";
$CONF{'LOCAL_CONF_URL'} = "svn/repo/local";

# Local Working Directories
$CONF{'LOG_DIR'} = "/tmp/var/log/pilu";
$CONF{'VAR_DIR'} = "/tmp/var/local/pilu";

# Now read the configuration file:
open FILE, $conf_file or die "Cannot open configuration file :$!";
while (<FILE>) {
	chomp;                  # no newline
	s/#.*//;                # no comments
	s/^\s+//;               # no leading white
	s/\s+$//;               # no trailing white
	next unless length;     # anything left?
	my ($var, $value) = split(/\s*=\s*/, $_, 2);
	$CONF{$var} = $value;
}
close FILE;


$CONF{'GLOBAL_PATH'} = "$CONF{'VAR_DIR'}/global";
$CONF{'LOCAL_PATH'} = "$CONF{'VAR_DIR'}/local";
$CONF{'NEW_PATH'} = "$CONF{'VAR_DIR'}/new";
$CONF{'OLD_PATH'} = "$CONF{'VAR_DIR'}/old";
$CONF{'VER_FILE'} = "$CONF{'VAR_DIR'}/version";
$CONF{'TMP_DIR'} = "/tmp";

# Complete URL's
$CONF{'GLOBAL_URL'} = 
				"$CONF{'PROTOCOL'}://$CONF{'SERVER'}/$CONF{'GLOBAL_CONF_URL'}";
$CONF{'LOCAL_URL'} = 
				"$CONF{'PROTOCOL'}://$CONF{'SERVER'}/$CONF{'LOCAL_CONF_URL'}";

#######################
# Configuration Ok. Now time to work.

# Create the directories
rmdir $CONF{'OLD_PATH'};
rmdir $CONF{'NEW_PATH'};
mkpath $CONF{'LOG_DIR'};
mkpath $CONF{'TMP_DIR'};
mkpath $CONF{'GLOBAL_PATH'};
mkpath $CONF{'LOCAL_PATH'};

&logger("pilu started");

my $ctxg = SVN::Client->new();
my $ctxl = SVN::Client->new();

## check the current version of both trees
my ($current_global_version,$current_local_version) = &get_current_version();

&logger("Current Global Configuration: $current_global_version");
&logger("Current Local Configuration: $current_local_version");




## make sure that we have the old configuration (for diff)
my $tmp1 = $ctxg->checkout($CONF{'GLOBAL_URL'}, $CONF{'GLOBAL_PATH'},
										$current_global_version, 1);
my $tmp2 = $ctxl->checkout($CONF{'LOCAL_URL'}, $CONF{'LOCAL_PATH'},
										$current_local_version, 1);

## Make the old conf tree
$ctxg->export($CONF{'GLOBAL_PATH'}, $CONF{'OLD_PATH'}, undef, 1);
$ctxl->export($CONF{'LOCAL_PATH'}, $CONF{'OLD_PATH'}, undef, 1);




## check out the latest version of both tree
my $latest_global_version = $ctxg->checkout($CONF{'GLOBAL_URL'}, 
																$CONF{'GLOBAL_PATH'}, 'HEAD', 1);
my $latest_local_version = $ctxl->checkout($CONF{'LOCAL_URL'}, 
																$CONF{'LOCAL_PATH'}, 'HEAD', 1);

## Make the new conf tree
$ctxg->export($CONF{'GLOBAL_PATH'}, $CONF{'NEW_PATH'}, undef, 1);
$ctxl->export($CONF{'LOCAL_PATH'}, $CONF{'NEW_PATH'}, undef, 1);




&logger("Latest Global Configuration: $latest_global_version");
&logger("Latest Local Configuration: $latest_local_version");

if (($latest_global_version == $current_global_version) &&
		($latest_local_version == $current_local_version)) {
	&logger("System is up to date, exiting");	
	exit;
} 



## We are here because the system is not up to date
my @old_packages_list = &read_to_array("$CONF{'OLD_PATH'}/packages.list");
my @new_packages_list = &read_to_array("$CONF{'NEW_PATH'}/packages.list");

my $packages_diff = Array::Diff->diff(\@old_packages_list,
																			\@new_packages_list);

my $add_list = $packages_diff->added();
my $rem_list = $packages_diff->deleted();




## Verify that the latest revision has been reached.
## If not, raise alarm
my $tmp = &verify_packages (@old_package_list);
if (!$tmp) {
	&logger ("Bungled up packages, something bad happened. Trying to recover");
	&install_packages("$CONF{'OLD_PATH'}/preseed.cfg", undef, @old_package_list);
}

&install_packages("$CONF{'NEW_PATH'}/preseed.cfg", @$rem_list, @$add_list);
my $tmp = &verify_packages (@new_package_list);
if (!$tmp) {
	&logger ("Bungled up packages, something bad happened. Trying to recover");
	&install_packages("$CONF{'NEW_PATH'}/preseed.cfg", undef, @new_package_list);
}




## Now is the time for configuration management.
## This can, in principle, replace the entire package management part.

# let's assume the existance of desc.global and desc.local to ease the things
# Read to 1D array first
my @old_global_desc = &read_to_array ("$CONF{'OLD_PATH'}/desc.global");
my @old_local_desc = &read_to_array ("$CONF{'OLD_PATH'}/desc.local");
my @new_global_desc = &read_to_array ("$CONF{'NEW_PATH'}/desc.global");
my @new_local_desc = &read_to_array ("$CONF{'NEW_PATH'}/desc.local");

# desc.{local|global} contains every file and directory, so
# no need to depend upon svn diff
my $global_conf_diff = Array::Diff->diff (\@old_global_desc,
																				\@new_global_desc);
my $local_conf_diff = Array::Diff->diff (\@old_local_desc,
																				\@new_local_desc);

my $global_conf_add_list = $global_conf_diff->added();
my $global_conf_rem_list = $global_conf_diff->deleted();
my $local_conf_add_list = $local_conf_diff->added();
my $local_conf_rem_list = $local_conf_diff->deleted();

push ( @$global_conf_add_list, @$local_conf_add_list);
push ( @$global_conf_rem_list, @$local_conf_rem_list);

my @conf_add_list = @$global_conf_add_list;
my @conf_rem_list = @$global_conf_rem_list;

&process_conf (@conf_add_list, @conf_rem_list);



## Write new version to the version file
print "Now write the new version to the version file\n";





sub process_conf {
	my (@add_list, @rem_list) = @_;
	
	# Let's not do anything with rem_list
	for $x (0 .. $#add_list) {
		my @tmp = split /\t+/, $add_list[$x];
		# Filename	MD5	Permission	Owner	Group	Bash_Hook
		# Filename can be either directory or file
		# If directory, just need to change the ownerships and permissions
		$fn = $tmp[0];
		if (-d "$CONF{'NEW_PATH'}/conf_tree$fn" ) {

			# A directory
			chmod (oct($tmp[2]), $fn);
			my ($uid,$gid) = &get_uid_gid($tmp[3],$tmp[4]);	
			mkpath $fn;
			chown $uid, $gid, $fn;	
			if ($tmp[5]) {
				&logger(qx($tmp[5]));
			}

		} elsif (-f "$CONF{'NEW_PATH'}/conf_tree$fn" ) {

			# A file
			File::Copy::copy("$CONF{'NEW_PATH'}/conf_tree$fn", $fn) or die "$!";
			chmod (oct($tmp[2]), $fn);
			my ($uid,$gid) = &get_uid_gid($tmp[3],$tmp[4]);	
			chown $uid, $gid, $fn;	
			if ($tmp[5]) {
				&logger(qx($tmp[5]));
			}

		} else {

			&logger ("Bungled configuration, file does not exist in Conf tree");

		}
	}
}

sub get_uid_gid {
	my ($uname, $gname) = @_;
	my ($login, $pass, $uid, $gid1) = getpwnam ($uname) or die "$!";
	my ($name, $passwd, $gid, $members) = getgrnam ($gname) or die "$!";
	return ($uid,$gid);
}

sub verify_packages {
	# Verify that the packages have been installed.
	my (@package_list) = @_;
	for $x (0 .. $#package_list) {
		my $tmp = qx(/usr/bin/dpkg -l | /bin/grep -w $package_list[$x]);
		if (!$tmp) {
			return 0;
		}
	}
	return 1;
}

sub read_to_array {
	# Reads the file into an array (packages.list)
	my ($file) = @_;
	open FILE, $file or die "Can not open $file : $!";
	my @ret_arr;
	while (<FILE>) {
	  chomp;                  # no newline
  	s/#.*//;                # no comments
	  s/^\s+//;               # no leading white
  	s/\s+$//;               # no trailing white
	  next unless length;     # anything left?
		next unless length;
		push (@ret_arr, ($_));
	}
	close FILE;
	return @ret_arr;
}

sub install_packages {
	# Install the packages specified in the packlist file
	# Assumes that the svn diff and everything else has
	# already been performed.
	my ($preseed, @rem_packages, @add_packages) = @_;
	if ($preseed) {
		$tmp = qx(/usr/bin/debconf-set-selections $preseed);
		&logger($tmp);
	}

	$tmp = qx(/usr/bin/dpkg --reconfigure -a);
	&logger($tmp);
	
	$tmp = qx(/usr/bin/apt-get -q install --force-yes);
	&logger($tmp);

	$tmp = qx(/usr/bin/apt-get -q update);
	&logger($tmp);

	$tmp = qx(/usr/bin/apt-get -q -f install -y);
	&logger($tmp);

	for $x (0 .. $#rem_packages) {
			$tmp = qx(/usr/bin/apt-get -q --purge remove $rem_packages[$x]);
			&logger($tmp);
	}
	for $x (0 .. $#add_packages) {
			$tmp = qx(/usr/bin/apt-get -q install -y $add_packages[$x]);
			&logger($tmp);
	}

	$tmp = qx(/usr/bin/apt-get -q --purge autoremove -y);
	&logger($tmp);
}

sub get_current_version {
	# This needs to be checked from the version file, not the local working copy.
	# It is possible that working copy is out of synch, maybe due to a crash.
	my $global = undef;
	my $local = undef;

	my $tmp = open FILE, $CONF{'VER_FILE'};
	if ($tmp) {
		while (<FILE>) {
			chomp;
			if ($. == 1) {
				$global = $_;
			} elsif ($. == 2) {
				$local = $_;
			} else { # I do not like somebody else messing with my stuff x(
				&logger ("Bungled Configuration");
				exit;
			}
		}
		close FILE;
	} else {
		# Let's take the versions to be 0
		$global = 0;
		$local = 0;
	}	
	return ($global,$local);
}

# The logging function 
sub logger {
	my ($str,$type) = @_;
	my $gen_log = "pilu.log";						# For general log
	my $pack_log = "pack.log";						# Package management
	my $conf_log = "conf_file.log";			# Configuration file modification
	my $exec_log = "exec.log";			# Hooks execution log

	my $filename = $gen_log;

	if ($type eq "exec") {
		$filename = $exec_log;
	} elsif ($type eq "pack") {
		$filename = $pack_log;
	}	elsif ($type eq "conf") {
		$filename = $conf_log;
	} else {
		$filename = $gen_log;
	}
	
	$filename = "$CONF{'LOG_DIR'}/$filename";
	open FILE, ">>$filename" or die "Can not open $filename : $!";
	$time = &get_system_time();
	print FILE "$time [pilu]: $str\n";
	close FILE;	
}

# The timestamp generate
sub get_system_time {
  ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
  return sprintf ("%4d-%02d-%02d %02d:%02d:%02d",
            $year+1900,$mon+1,$mday,$hour,$min,$sec);
}

